/**
 * Copyright (c) 2008 Andrew Rapp. All rights reserved.
 *  
 * This file is part of XBee-API.
 *  
 * XBee-API is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * XBee-API is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *  
 * You should have received a copy of the GNU General Public License
 * along with XBee-API.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.rapplogic.xbee.api;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;

import com.rapplogic.xbee.api.wpan.RxResponse16;
import com.rapplogic.xbee.api.wpan.RxResponse64;
import com.rapplogic.xbee.api.wpan.RxResponseIoSample;
import com.rapplogic.xbee.api.wpan.TxStatusResponse;
import com.rapplogic.xbee.api.zigbee.ZNetExplicitRxResponse;
import com.rapplogic.xbee.api.zigbee.ZNetNodeIdentificationResponse;
import com.rapplogic.xbee.api.zigbee.ZNetRxIoSampleResponse;
import com.rapplogic.xbee.api.zigbee.ZNetRxResponse;
import com.rapplogic.xbee.api.zigbee.ZNetTxStatusResponse;
import com.rapplogic.xbee.util.ByteUtils;
import com.rapplogic.xbee.util.IIntInputStream;
import com.rapplogic.xbee.util.InputStreamWrapper;
import com.rapplogic.xbee.util.IntArrayOutputStream;

/**
 * Reads a packet from the input stream, verifies checksum and creates an
 * XBeeResponse object
 * <p/>
 * Notes:
 * <p/>
 * Escaped bytes increase packet length but packet stated length only indicates
 * un-escaped bytes. Stated length includes all bytes after Length bytes, not
 * including the checksum
 * <p/>
 * 
 * Backported to Java 1.4
 * 
 * @author Andrew Rapp
 * @author barciszewski@gmail.com backport refactoring
 * @author perkins.steve@gmail.com backport refactoring
 * 
 */
public class PacketParser implements IIntInputStream, IPacketParser {

	private final static Logger log = Logger.getLogger(PacketParser.class);

	private IIntInputStream in;

	// size of packet after special bytes have been escaped
	private XBeePacketLength length;
	private Checksum checksum = new Checksum();

	private boolean done = false;

	private int bytesRead;
	private int escapeBytes;

	private XBeeResponse response;
	private ApiId apiId;
	private int intApiId;
	// Integer, Class<? extends XBeeResponse>
	private static Map handlerMap = new HashMap();

	// TODO reuse this object for all packets

	// experiment to preserve original byte array for transfer over network
	// (Starts with length)
	private IntArrayOutputStream rawBytes = new IntArrayOutputStream();

	static {
		// TODO put all response handlers in specific packet and load all.
		// implement static handlesApi method
		handlerMap.put(ApiId.AT_RESPONSE.getValue(), AtCommandResponse.class);
		handlerMap.put(ApiId.MODEM_STATUS_RESPONSE.getValue(),
				ModemStatusResponse.class);
		handlerMap.put(ApiId.REMOTE_AT_RESPONSE.getValue(),
				RemoteAtResponse.class);
		handlerMap.put(ApiId.RX_16_IO_RESPONSE.getValue(),
				RxResponseIoSample.class);
		handlerMap.put(ApiId.RX_64_IO_RESPONSE.getValue(),
				RxResponseIoSample.class);
		handlerMap.put(ApiId.RX_16_RESPONSE.getValue(), RxResponse16.class);
		handlerMap.put(ApiId.RX_64_RESPONSE.getValue(), RxResponse64.class);
		handlerMap.put(ApiId.TX_STATUS_RESPONSE.getValue(),
				TxStatusResponse.class);
		handlerMap.put(ApiId.ZNET_EXPLICIT_RX_RESPONSE.getValue(),
				ZNetExplicitRxResponse.class);
		handlerMap.put(ApiId.ZNET_IO_NODE_IDENTIFIER_RESPONSE.getValue(),
				ZNetNodeIdentificationResponse.class);
		handlerMap.put(ApiId.ZNET_IO_SAMPLE_RESPONSE.getValue(),
				ZNetRxIoSampleResponse.class);
		handlerMap.put(ApiId.ZNET_RX_RESPONSE.getValue(), ZNetRxResponse.class);
		handlerMap.put(ApiId.ZNET_TX_STATUS_RESPONSE.getValue(),
				ZNetTxStatusResponse.class);
	}

	static void registerResponseHandler(int apiId, Class clazz) {
		Integer apiKey = Integer.valueOf(apiId);
		if (handlerMap.get(apiKey) == null) {
			log.info("Registering response handler " + clazz.getCanonicalName()
					+ " for apiId: " + apiKey);
		} else {
			log.warn("Overriding existing implementation: "
					+ ((Class) handlerMap.get(apiKey)).getCanonicalName()
					+ ", with " + clazz.getCanonicalName() + " for apiId: "
					+ apiKey);
		}

		handlerMap.put(apiKey, clazz);
	}

	static void unRegisterResponseHandler(int apiId) {
		Integer apiKey = Integer.valueOf(apiId);
		if (handlerMap.get(apiKey) != null) {
			if (log.isInfoEnabled()) {
				log.info("Unregistering response handler "
						+ ((Class) handlerMap.get(apiKey)).getCanonicalName()
						+ " for apiId: " + apiKey);
			}
			handlerMap.remove(apiKey);
		} else {
			throw new IllegalArgumentException("No response handler for: "
					+ apiKey);
		}
	}

	public PacketParser(InputStream in) {
		this.in = new InputStreamWrapper(in);
	}

	// for parsing a packet from a byte array
	public PacketParser(IIntInputStream in) {
		this.in = in;
	}

	/**
	 * This method is guaranteed (unless I screwed up) to return an instance of
	 * XBeeResponse and should never throw an exception If an exception occurs,
	 * it will be packaged and returned as an ErrorResponse.
	 * 
	 * @return
	 */
	public XBeeResponse parsePacket() {

		Exception exception = null;

		try {
			// BTW, length doesn't account for escaped bytes
			int msbLength = this.read("Length MSB");
			int lsbLength = this.read("Length LSB");

			// length of api structure, starting here (not including start byte
			// or length bytes, or checksum)
			this.length = new XBeePacketLength(msbLength, lsbLength);

			log.debug("packet length is "
					+ ByteUtils.formatByte(length.getLength()));

			// total packet length = stated length + 1 start byte + 1 checksum
			// byte + 2 length bytes

			intApiId = this.read("API ID");
			Integer apiKey = Integer.valueOf(intApiId);

			this.apiId = ApiId.get(apiKey);

			if (apiId == null) {
				this.apiId = ApiId.UNKNOWN;
			}

			log.info("Handling ApiId: " + apiId);

			// TODO parse I/O data page 12. 82 API Identifier Byte for 64 bit
			// address A/D data (83 is for 16bit A/D data)
			// TODO XBeeResponse should implement an abstract parse method

			Iterator iter = handlerMap.keySet().iterator();
			while (iter.hasNext()) {
				Integer handlerApiId = (Integer) iter.next();
				if (handlerApiId.equals(apiKey)) {
					if (log.isDebugEnabled()) {
						log.debug("Found response handler for apiId ["
								+ ByteUtils.toBase16(intApiId)
								+ "]: "
								+ ((Class) handlerMap.get(handlerApiId))
										.getCanonicalName());
					}
					response = (XBeeResponse) ((Class) handlerMap
							.get(handlerApiId)).newInstance();
					response.parse(this);
					break;
				}
			}

			if (response == null) {
				log.info("Did not find a response handler for ApiId ["
						+ ByteUtils.toBase16(intApiId)
						+ "].  Returning GenericResponse");
				response = new GenericResponse();
				response.parse(this);
			}

			response.setChecksum(this.read("Checksum"));

			if (!this.isDone()) {
				throw new XBeeParseException(
						"There are remaining bytes according to stated packet length but we have read all the bytes we thought were required for this packet (if that makes sense)");
			}

			response.finish();
		} catch (Exception e) {
			// added bytes read for troubleshooting
			log.error(
					"Failed due to exception.  Returning ErrorResponse.  bytes read: "
							+ ByteUtils.toBase16(rawBytes.getIntArray()), e);
			exception = e;

			response = new ErrorResponse();

			((ErrorResponse) response).setErrorMsg(exception.getMessage());
			// but this isn't
			((ErrorResponse) response).setException(e);
		} finally {
			response.setLength(length);
			response.setApiId(apiId);
			// preserve original byte array for transfer over networks
			response.setRawPacketBytes(rawBytes.getIntArray());
		}

		return response;
	}

	/**
	 * Same as read() but logs the context of the byte being read. useful for
	 * debugging
	 */
	public int read(String context) throws IOException {
		int b = this.read();
		log.debug("Read " + context + " byte, val is "
				+ ByteUtils.formatByte(b));
		return b;
	}

	/**
	 * This method should only be called by read()
	 * 
	 * @throws IOException
	 */
	private int readFromStream() throws IOException {
		int b = in.read();
		// save raw bytes to transfer via network
		rawBytes.write(b);

		return b;
	}

	/**
	 * This method reads bytes from the underlying input stream and performs the
	 * following tasks: 1. Keeps track of how many bytes we've read 2.
	 * Un-escapes bytes if necessary and verifies the checksum.
	 */
	public int read() throws IOException {

		if (done) {
			throw new XBeeParseException("Packet has read all of its bytes");
		}

		int b = this.readFromStream();

		if (b == -1) {
			throw new XBeeParseException(
					"Read -1 from input stream while reading packet!");
		}

		if (XBeePacket.isSpecialByte(b)) {
			log.debug("Read special byte that needs to be unescaped");

			if (SpecialByte.ESCAPE.getValue().intValue() == b) {
				log.debug("found escape byte");
				// read next byte
				b = this.readFromStream();

				log.debug("next byte is " + ByteUtils.formatByte(b));
				b = 0x20 ^ b;
				log.debug("unescaped (xor) byte is " + ByteUtils.formatByte(b));

				escapeBytes++;
			} else {
				// TODO some responses such as AT Response for node discover do
				// not escape the bytes?? shouldn't occur if AP mode is 2?
				// while reading remote at response Found unescaped special byte
				// base10=19,base16=0x13,base2=00010011 at position 5
				log
						.warn("Found unescaped special byte "
								+ ByteUtils.formatByte(b) + " at position "
								+ bytesRead);
			}
		}

		bytesRead++;

		// do this only after reading length bytes
		if (bytesRead > 2) {

			// when verifying checksum you must add the checksum that we are
			// verifying
			// checksum should only include unescaped bytes!!!!
			// when computing checksum, do not include start byte, length, or
			// checksum; when verifying, include checksum
			checksum.addByte(b);

			log.debug("Read byte " + ByteUtils.formatByte(b) + " at position "
					+ bytesRead + ", packet length is "
					+ this.length.get16BitValue() + ", #escapeBytes is "
					+ escapeBytes + ", remaining bytes is "
					+ this.getRemainingBytes());

			// escape bytes are not included in the stated packet length
			if (this.getFrameDataBytesRead() >= (length.get16BitValue() + 1)) {
				// this is checksum and final byte of packet
				done = true;

				log.debug("Checksum byte is " + b);

				if (!checksum.verify()) {
					throw new XBeeParseException(
							"Checksum is incorrect.  Expected 0xff, but got "
									+ checksum.getChecksum());
				}
			}
		}

		return b;
	}

	/**
	 * Reads all remaining bytes except for checksum
	 * 
	 * @return
	 * @throws IOException
	 */
	public int[] readRemainingBytes() throws IOException {

		// minus one since we don't read the checksum
		int[] value = new int[this.getRemainingBytes() - 1];

		log.debug("There are " + value.length + " remaining bytes");

		for (int i = 0; i < value.length; i++) {
			value[i] = this.read("Remaining bytes " + i);
		}

		return value;
	}

	public XBeeAddress64 parseAddress64() throws IOException {
		XBeeAddress64 addr = new XBeeAddress64();

		for (int i = 0; i < 8; i++) {
			addr.getAddress()[i] = this.read("64-bit Address byte " + i);
		}

		return addr;
	}

	public XBeeAddress16 parseAddress16() throws IOException {
		XBeeAddress16 addr16 = new XBeeAddress16();

		addr16.setMsb(this.read("Address 16 MSB"));
		addr16.setLsb(this.read("Address 16 LSB"));

		return addr16;
	}

	/**
	 * Returns number of bytes remaining, relative to the stated packet length
	 * (not including checksum).
	 * 
	 * @return
	 */
	public int getFrameDataBytesRead() {
		// subtract out the 2 length bytes
		return this.getBytesRead() - 2;
	}

	/**
	 * Number of bytes remaining to be read, including the checksum
	 * 
	 * @return
	 */
	public int getRemainingBytes() {
		// add one for checksum byte (not included) in packet length
		return this.length.get16BitValue() - this.getFrameDataBytesRead() + 1;
	}

	// get unescaped packet length
	// get escaped packet length

	/**
	 * Does not include any escape bytes
	 * 
	 * @return
	 */
	public int getBytesRead() {
		return bytesRead;
	}

	public void setBytesRead(int bytesRead) {
		this.bytesRead = bytesRead;
	}

	public boolean isDone() {
		return done;
	}

	public void setDone(boolean done) {
		this.done = done;
	}

	public int getChecksum() {
		return checksum.getChecksum();
	}

	public XBeePacketLength getLength() {
		return length;
	}

	public ApiId getApiId() {
		return apiId;
	}

	public int getIntApiId() {
		return this.intApiId;
	}
}
